﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Text.RegularExpressions;
using System.IO;
using System.Threading;
using System.Net;
using System.Windows.Forms;
using System.Drawing;
using System.Data;
using System.Net.Cache;
using System.Reflection;
using System.Runtime.Serialization.Formatters.Binary;

namespace MulticastTV
{
    /// <summary>
    /// Web Epg scrapping class.
    /// This class is responsible for downloading web pages with epg information.
    /// It is used to get today's shows list with links to details for a particular channel.
    /// For particular show details (like genre, summary etc.) EpgDetails class is used.
    /// Parsing of recevied data is done in webepgplugins.
    /// TODO: Log exceptions.
    /// TODO: Weekly epg!
    /// </summary>
    public class Epg
    {
        public string AssemblyVersion
        {
            get
            {
                return Assembly.GetExecutingAssembly().GetName().Version.ToString();
            }
        }

        // After web page is parsed, we raise event for gui to update.
        public delegate void EpgFinishedHandler(object sender, EpgArgs ea);
        public event EpgFinishedHandler Finished;

        ManualResetEvent mre = new ManualResetEvent(true);

        const int DefaultTimeout = 8000; //  8s timeout
        string[] linedelimiter = { "\n" };
        string CachePath = Globals.UserAppPath+@"\..\cache\list\";
        List<string> CacheList = new List<string>();
        public epgResult result;
        HttpRequestCachePolicy policy;
        CookieContainer CookieCont;
        HttpWebRequest httpWebRequest=null;
        Types.AvailablePlugin selectedPlugin;
        ASCIIEncoding encoding = new ASCIIEncoding();
        FileSystemWatcher fwatcher=null;
        bool VersionOk = false;

        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="path_">Startup path</param>
        public Epg()
        {
            // this instance is created only if dll listed in settings exists
            selectedPlugin = GlobalP.Plugins.AvailablePlugins.Find(Properties.Settings.Default.EPGPlugin);
            string wdir = "";
            try
            {
                wdir = Path.GetDirectoryName(Properties.Settings.Default.XmltvFile);
            }
            catch { }

            if (AssemblyVersion.CompareTo(selectedPlugin.Instance.MulticasttvVersion) >= 0)
            {
                VersionOk = true;
            }

            if (selectedPlugin.Instance.PluginType == "offline" && selectedPlugin.Instance.Name =="xmltv" && Directory.Exists(wdir))
            {
                fwatcher = new FileSystemWatcher();
                fwatcher.Path = wdir;
                fwatcher.Filter = Path.GetFileName(Properties.Settings.Default.XmltvFile);
                fwatcher.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName; 
                fwatcher.Changed += new FileSystemEventHandler(Changed);
                fwatcher.Created += new FileSystemEventHandler(FileCreated);
                fwatcher.EnableRaisingEvents = true;
            }
            
            pluginNames.FileName = selectedPlugin.Instance.Name;
            pluginNames.loadNames(false);
            policy = new HttpRequestCachePolicy(HttpRequestCacheLevel.Default);
            CookieCont = new CookieContainer();
            HttpWebRequest.DefaultCachePolicy = policy;
        }

        ~Epg()
        {
            CacheList.Clear();
            if (fwatcher!= null)
                fwatcher.Dispose();
        }

        /// <summary>
        /// Start downloading in new thread
        /// </summary>
        /// <param name="chan">Channel name</param>
        public void start(string ChanName, epgResult er)
        {
            result = er;
            ThreadStart starter = delegate { getEpg(ChanName); };
            Thread th =  new Thread(starter);
            th.IsBackground = true;
            th.Start();
        }
        
        /// <summary>
         /// Get today's epg information for current channel
         /// </summary>
         /// <param name="chan">Channel name</param>
        private void getEpg(string chan)
        {
            bool recvsign = mre.WaitOne(DefaultTimeout+500 ,true);
            if (!recvsign)
                return;// should never happen
            
            mre.Reset();

            if (!VersionOk)
            {
                mre.Set();
                return;
            }

            if (result.EpgData != null)
                result.EpgData.Clear();
            
            if (selectedPlugin == null)
            {
                mre.Set();
                Finished(this, new EpgArgs("no_data"));
                return;
            }
            
            bool xmltvparser=false;
            if (selectedPlugin.Instance.PluginType == "offline" && selectedPlugin.Instance.Name == "xmltv")
                xmltvparser = true;

            if (xmltvparser)
                result.DatetimeFormat = true;

            string ChanId="Undefined";
            if (chan != null && !pluginNames.channel.TryGetValue(chan.ToUpper(), out ChanId))
            {
                mre.Set();
                Finished(this, new EpgArgs("no_data"));
                return;
            }

            // check for cache
            if (CacheList.Contains(chan))
            {
                string cachedFile = CachePath + validateFileName(chan + ".txt");
                if (File.Exists(cachedFile))
                {
                    FileStream fs = null;
                    try
                    {
                        fs = new FileStream(cachedFile, FileMode.Open);
                        BinaryFormatter formatter = new BinaryFormatter();
                        result.EpgData = (List<string[]>)formatter.Deserialize(fs);
                        fs.Close();
                        //raise finished event
                        Finished(this, new EpgArgs("epg_finished"));
                    }
                    catch
                    {
                        if (fs != null)
                            fs.Close();
                    }
                }
            }
            else
            {
                if (selectedPlugin.Instance.PluginType == "online")
                {
                    sendRequest(chan, ChanId);
                }
                else if (selectedPlugin.Instance.PluginType == "offline" && xmltvparser)
                {
                    // use xmltv parser
                    if (XmltvParser.xmltvparser.Start())
                    {
                        result.EpgData = XmltvParser.xmltvparser.Parse(chan, ChanId);
                        if (result.EpgData.Count > 0)
                        {
                            result.EpgData.Add(new string[] { DateTime.Now.ToString("yyy-MM-dd HH:mm", null) });
                            Finished(this, new EpgArgs("epg_finished"));
                            //cacheData(chan); // ??? no need for cache - whole xml is loaded in memory.
                        }
                        else
                            Finished(this, new EpgArgs("no_data"));
                    }
                }
            }
            mre.Set();
        }

        private void sendRequest(string chan,string chanId)
        {
           
            if (selectedPlugin.Instance.InitialRequestNeeded)
            {
                try
                {
                    httpWebRequest = (HttpWebRequest)WebRequest.Create(selectedPlugin.Instance.HostName + selectedPlugin.Instance.InitialRequest);
                }
                catch 
                {
                    Finished(this, new EpgArgs("no_data"));
                    return;
                }
                httpWebRequest.Referer = selectedPlugin.Instance.HostName;
                httpWebRequest.Method = selectedPlugin.Instance.InitialRequestMethod;
                httpWebRequest.Timeout = DefaultTimeout;
                if (Properties.Settings.Default.UserAgent == "")
                    httpWebRequest.UserAgent = "MulticastTV/" + AssemblyVersion + " (compatible; MSIE 7.0; Windows NT 5.1)";
                else
                    httpWebRequest.UserAgent = Properties.Settings.Default.UserAgent;
                httpWebRequest.CookieContainer = CookieCont;

                if (httpWebRequest.Method == "POST")
                {
                    byte[] byte1 = encoding.GetBytes(selectedPlugin.Instance.InitialRequest);
                    httpWebRequest.ContentLength = byte1.Length;
                    Stream newStream = httpWebRequest.GetRequestStream();
                    newStream.Write(byte1, 0, byte1.Length);
                    newStream.Close();
                }
                if (downloadEpg(chan, chanId, httpWebRequest, true))
                    selectedPlugin.Instance.InitialRequestNeeded = false;      
            }

            string[] reqparm = selectedPlugin.Instance.GetTodayRequest(chanId);
            try
            {
                httpWebRequest = (HttpWebRequest)WebRequest.Create(selectedPlugin.Instance.HostName + reqparm[0]);
            }
            catch
            {
                Finished(this, new EpgArgs("no_data"));
                return;
            }
            httpWebRequest.Referer = reqparm[1];
            httpWebRequest.Method = selectedPlugin.Instance.RequestMethod;
            httpWebRequest.Timeout = DefaultTimeout;
            if (Properties.Settings.Default.UserAgent == "")
                httpWebRequest.UserAgent = "MulticastTV/" + AssemblyVersion + " (compatible; MSIE 7.0; Windows NT 5.1)";
            else
                httpWebRequest.UserAgent = Properties.Settings.Default.UserAgent;
            httpWebRequest.CookieContainer = CookieCont;
            httpWebRequest.ContentType = "application/x-www-form-urlencoded";

            string line = reqparm[2];
            if (httpWebRequest.Method == "POST")
            {
                byte[] byte1 = encoding.GetBytes(line);
                httpWebRequest.ContentLength = byte1.Length;
                Stream newStream = httpWebRequest.GetRequestStream();
                newStream.Write(byte1, 0, byte1.Length);
                newStream.Close();
            }

            if (downloadEpg(chan, chanId, httpWebRequest, false))
            {
                Finished(this, new EpgArgs("epg_finished"));
            }
        }

        private bool downloadEpg(string chan,
                                 string id, 
                                 HttpWebRequest req,
                                 bool initial)
        {
            HttpWebResponse resp =null;
            try
            {
                resp = (HttpWebResponse)req.GetResponse();
            }
            catch 
            {
                req = null;
                return false;
            }
            Stream RespStream = null;
            try
            {
                RespStream = resp.GetResponseStream();
            }
            catch 
            {
                if (resp != null)
                    resp.Close();
                return false;
            }

            StreamReader sr = new StreamReader(RespStream);
            string epgPage=null;
            try
            {
                epgPage = sr.ReadToEnd();
            }
            catch
            {
            }

            // close everything
            sr.Dispose();
            RespStream.Close();
            if (resp != null)
                resp.Close();

            if (epgPage != null)
            {
                if (initial)
                    selectedPlugin.Instance.ParseInitPage(epgPage);
                else
                {
                    result.EpgData = selectedPlugin.Instance.ParseToday(epgPage);
                    // add reference date
                    if (result.EpgData.Count > 0)
                        result.EpgData.Add(new string[] { DateTime.Now.ToString("yyyy-MM-dd HH:mm", null) });
                    //raise finished event
                    cacheData(chan);
                    return true;
                }   
            }
            return false;
        }

        private void cacheData(string chanName)
        {
            //Cache received data
            if (!Directory.Exists(CachePath))
                Directory.CreateDirectory(CachePath);

            if (!CacheList.Contains(chanName))
            {
                FileStream fs = new FileStream(CachePath + validateFileName(chanName + ".txt"), FileMode.Create);
                try
                {
                    //add to list
                    CacheList.Add(chanName);
                    //save to disk
                    BinaryFormatter formatter = new BinaryFormatter();
                    formatter.Serialize(fs, result.EpgData);

                    fs.Close();
                }
                catch
                {
                    fs.Close();
                }
            }
        }

        public void clearCache(string chanName)
        {
            if (CacheList.Contains(chanName))
            {
                CacheList.Remove(chanName);
                // if plugin needs initial request, we have to reset it.
                if (selectedPlugin.Instance.InitialRequest.Length>0)
                    selectedPlugin.Instance.InitialRequestNeeded = true;
            }
        }

        public static string validateFileName(string file)
        { 
            char[] ic = Path.GetInvalidFileNameChars();
            foreach (char character in ic)
            {
                file = file.Replace(character,'-');
            }
            return file;
        }

        public void Clean()
        {
            httpWebRequest = null;
            if (selectedPlugin!=null)
                selectedPlugin.Instance.Dispose();
        }
       
        private void Changed(object sender, FileSystemEventArgs e)
        {
            XmltvParser.xmltvparser.reload();
        }

        private void FileCreated(object sender, FileSystemEventArgs e)
        {
            XmltvParser.xmltvparser.reload();
        }

    }
    public class epgResult
    {
        private List<string[]> epg;
        private string ip;
        private string name;
        private string referer;
        private bool datetimeFormat;

        public epgResult(string _ip, string _name, string _sender)
        {
            ip = _ip;
            name = _name;
            referer = _sender;
            datetimeFormat = false;
        }
        
        public List<string[]> EpgData
        {
            get { return epg; }
            set { epg = value; }
        }

        public string Ip
        {
            get { return ip; }
            set { ip = value; }
        }

        public string Name
        {
            get { return name; }
            set { name = value; }
        }
        
        public string Referer
        {
            get { return referer; }
            set { referer = value; }
        }

        public bool DatetimeFormat
        {
            get { return datetimeFormat; }
            set { datetimeFormat = value; }
        }
    }


    /// <summary>
    /// This class manages list of channel names/aliases and their web identifications
    /// File with channel aliases and their web identifications must be placed in /parse folder and must have the same name as plugin with .txt extension.
    /// </summary>
    public static class pluginNames
    {
       private static Dictionary<string, string> names;
       private static DataTable pluginDt = new DataTable("epgnames"); 
       static string path = Application.StartupPath;
       static string xmlfile = "";
       static string origfile = "";
       static string pname = "";

       /// <summary>
       /// Constructor
       /// </summary>
       static pluginNames()
      {
            DataColumn DcName = new DataColumn();
            DcName.DataType = typeof(String);
            DcName.ColumnName = "cname";
            DcName.DefaultValue = "";
            DataColumn DcWname = new DataColumn();
            DcWname.DataType = typeof(String);
            DcWname.ColumnName = "wname";
            DcWname.DefaultValue = "";
            pluginDt.Columns.Add(DcName);
            pluginDt.Columns.Add(DcWname);
      }

       /// <summary>
       /// Plugin name
       /// </summary>
       public static string FileName
       {
           get { return origfile; }
           set {
                 xmlfile = Globals.UserAppPath + @"\..\"+value+".xml";
                 origfile = path + @"\parse\"+value+".txt";
                 pname = value;
           }
       }

        /// <summary>
        /// Populate channel/id dictionary from current table
        /// </summary>
        /// <param name="update">Update</param>
        public static void loadNames(bool update)
       {
           names = new Dictionary<string, string>(StringComparer.CurrentCultureIgnoreCase);

           if (!update)
            readFromFile();

           if (names.Count>0)
                names.Clear();
           foreach (DataRow dr in pluginDt.Rows)
           {
               string key = dr[0].ToString().ToUpper();
               if (!names.ContainsKey(key))
               {
                   if (dr[1].ToString() != "")
                       names.Add(dr[0].ToString().ToUpper(), dr[1].ToString());
               }
           }
       }

        /// <summary>
        /// Populate dictionary from internal xml, if xml doesn't exists it reads from original txt or xmltv file 
        /// </summary>
        public static void readFromFile()
        {
            if (pluginDt.Rows.Count>0)
                pluginDt.Rows.Clear();

            // if internal xml doesn't exist, we load it from txt file.
            if (!File.Exists(xmlfile))
                loadFromCustomFile("");
            // else we read from internal xml.
            if (pluginDt.Rows.Count > 0)
                pluginDt.Rows.Clear();
            if (xmlfile != "")
                pluginDt.ReadXml(xmlfile);
            
            updateChannels();
        }

        /// <summary>
        /// Populate dictionary from custom file (plugin_name.txt)
        /// </summary>
        public static void loadFromCustomFile(string customfile)
        {
            string fileToLoadFrom = "";
            bool readfromxmltv = false;
           
            // Auto load - based on plugin name
            if (customfile == "")
            {
                // If xmltv plugin is selected and xmltv file exists, read from xmltv.
                if (pname == "xmltv" && File.Exists(Properties.Settings.Default.XmltvFile))
                {
                    readfromxmltv = true;
                    fileToLoadFrom = Properties.Settings.Default.XmltvFile;
                }
                else
                    fileToLoadFrom = origfile;
            }
            else
            {
                fileToLoadFrom = customfile;
                if (Path.GetExtension(fileToLoadFrom) == ".xml")
                    readfromxmltv = true;  //most propably
            }
            
            if (fileToLoadFrom == "")
                return;

            if (!File.Exists(fileToLoadFrom))
                return;
            
            if (pluginDt.Rows.Count > 0)
                pluginDt.Rows.Clear();
            
            //add all channels from playlist
            foreach (DataRow drc in ChannelTable.menu.Tables["Menu"].Rows)
            { 
                  DataRow dr = pluginDt.NewRow();
                  dr[0] = drc[2].ToString();
                  dr[1] = "";
                  pluginDt.Rows.Add(dr);
            }

            if (!readfromxmltv)
            {
                //update with ids from txt file.
                StreamReader sr = new StreamReader(fileToLoadFrom, Encoding.UTF8);
                char[] eq = { '=' };
                string line = sr.ReadLine();
                while (line != null)
                {
                    string[] linedata = line.Split(eq);
                    if (linedata.Length < 2)
                        continue;
                    string id = linedata[1];
                    string name = linedata[0];
                    foreach (DataRow dr2 in pluginDt.Rows)
                    {
                        if (dr2[0].ToString().ToUpper() == name.ToUpper())
                        {
                            dr2[1] = id;
                            break;
                        }
                    }
                    line = sr.ReadLine();
                }
                sr.Close();
            }
            else
                XmltvParser.xmltvparser.loadIds(pluginDt, fileToLoadFrom);

            string wfile = xmlfile;
            if (wfile != "")
                pluginDt.WriteXml(wfile);
        }

        /// <summary>
        /// Store channel names vs web ids to xml file.
        /// </summary>
        public static void savePluginNames()
        {
            string wfile = xmlfile;
            if (pluginDt != null && pluginDt.Rows.Count >0 && wfile != "" )
                pluginDt.WriteXml(wfile);
        }

        /// <summary>
        /// Update current DataTable with channels that are not listed in txt file.
        /// </summary>
        public static void updateChannels()
        {
            //add new channels
            foreach (DataRow drc in ChannelTable.menu.Tables["Menu"].Rows)
            {
                bool found = false;
                foreach (DataRow drs in pluginDt.Rows)
                {
                    if (drc[2].ToString().ToUpper() == drs[0].ToString().ToUpper())
                        found = true;
                }
                if (!found)
                {
                    DataRow dr = pluginDt.NewRow();
                    dr[0] = drc[2].ToString();
                    dr[1] = "";
                    pluginDt.Rows.Add(dr);
                }
            }
        }

        /// <summary>
        /// Dictionary with channel names and their web id strings.
        /// </summary>
        public static Dictionary<string, string> channel 
        {
            get { return names; }
        }

        /// <summary>
        /// Datatable with channel names from playlist and their web ids
        /// </summary>
        public static DataTable PluginDt
        {
            get { return pluginDt; }
            set { pluginDt = value; }
        }
    }
}
